/*
 * Copyright (C) 2006 Johan Maasing johan at zoom.nu Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
 * or agreed to in writing, software distributed under the License is
 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package nu.zoom.swing.desktop.component.stringmenu.impl;

import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;

import javax.swing.Icon;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JSeparator;

import nu.zoom.swing.action.AbstractTypedAction;
import nu.zoom.swing.desktop.Workbench;
import nu.zoom.swing.desktop.WorkbenchListener;
import nu.zoom.swing.desktop.common.BackendException;
import nu.zoom.swing.desktop.component.stringmenu.StringMenu;
import nu.zoom.swing.desktop.component.stringmenu.StringMenuItem;
import nu.zoom.swing.desktop.component.stringmenu.StringMenuListener;
import nu.zoom.swing.desktop.preferences.InvalidDataTypeException;
import nu.zoom.swing.desktop.preferences.Preferences;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class StringMenuImpl<T extends Comparable<T> & Serializable> implements
		StringMenu<T>, WorkbenchListener {
	private final Log log = LogFactory.getLog(getClass());

	private final HashMap<StringMenuItem<T>, JMenuItem> itemStringToJMenuItem = new HashMap<StringMenuItem<T>, JMenuItem>();

	private final ArrayList<StringMenuListener<T>> listeners = new ArrayList<StringMenuListener<T>>();

	private final ArrayList<StringMenuItem<T>> itemStrings = new ArrayList<StringMenuItem<T>>();

	private JMenu menu = null;

	private final Preferences preferences;

	private final String preferencesKey;

	/**
	 * Instantiate a new menu
	 * 
	 * @param preferences
	 * @param workbench
	 * @param preferencesKey
	 * @param menuName
	 */
	StringMenuImpl(Preferences preferences, Workbench workbench,
			String preferencesKey) {
		this.preferences = preferences;
		this.preferencesKey = preferencesKey;
		log.trace("New String menu is registering as workbench listener");
		workbench.addWorkBenchListener(this);
	}

	@Override
	public synchronized void addListener(StringMenuListener<T> listener) {
		if (listener == null) {
			throw new IllegalArgumentException("Listener may not be null");
		}
		log.trace("Registering a string menu listener: " + listener);
		listeners.add(listener);
	}

	@Override
	public synchronized void removeListener(StringMenuListener<T> listener) {
		log.trace("Removing a string menu listener: " + listener);
		listeners.remove(listener);
	}

	@Override
	public synchronized JMenu getJMenu(final String menuName, final Icon icon,
			final String clearName, final String clearTooltip) {
		if (menu == null) {
			createJMenu(menuName, icon, clearName, clearTooltip);
			restoreMenu();
		}
		return menu;
	}

	@Override
	public synchronized void addItem(final StringMenuItem<T> menuItem) {
		if (menuItem == null) {
			log.fatal("Item may not be null");
			throw new IllegalArgumentException("Item may not be null");
		}
		if (!EventQueue.isDispatchThread()) {
			log.fatal("Must be called on the EventQueue dispatch thread");
			throw new IllegalStateException(
					"Must be called on the EventQueue dispatch thread");
		}
		if (!itemStringToJMenuItem.containsKey(menuItem)) {
			log.trace("Adding item to menu");
			addItemInternal(menuItem);
		}
	}

	@Override
	public synchronized void removeItem(StringMenuItem<T> menuItem) {
		if (!EventQueue.isDispatchThread()) {
			log.fatal("Must be called on the EventQueue dispatch thread");
			throw new IllegalStateException(
					"Must be called on the EventQueue dispatch thread");
		}

		if (menuItem != null) {
			log.trace("Looking for menu item to remove");
			final JMenuItem cachedJMenuItem = itemStringToJMenuItem
					.get(menuItem);
			log.trace("Cache returned item: " + cachedJMenuItem);
			if (cachedJMenuItem != null) {
				log.trace("Removing item from JMenu");
				this.menu.remove(cachedJMenuItem);

				log.trace("Removing item from key list");
				itemStrings.remove(menuItem);

				log.trace("Removing item from cache");
				itemStringToJMenuItem.remove(menuItem);

				log.trace("Removing ALL action listeners from JMenuItem");
				ActionListener[] listeners = cachedJMenuItem
						.getActionListeners();
				for (ActionListener listener : listeners) {
					cachedJMenuItem.removeActionListener(listener);
				}
			}
		}
	}

	private synchronized void fireMenuItemSelected(
			final StringMenuItem<T> menuItem) {
		for (final StringMenuListener<T> listener : listeners) {
			EventQueue.invokeLater(new Runnable() {
				public void run() {
					log.trace("Informing listener:" + listener + " that item: "
							+ menuItem + " was selected");
					listener.menuItemSelected(menuItem);
				}
			});
		}
	}

	@Override
	public synchronized void close() {
		try {
			log.trace("Trying to serialize key list to preferences");
			ByteArrayOutputStream baos = new ByteArrayOutputStream();
			ObjectOutputStream outs = new ObjectOutputStream(baos);
			outs.writeObject(itemStrings);
			outs.flush();

			preferences.setBytes(preferencesKey, baos.toByteArray());
		} catch (BackendException e) {
			log.warn(e);
		} catch (IOException e) {
			log.warn(e);
		}
	}

	@Override
	public void start() {
	}

	@Override
	public void visible() {
	}

	@SuppressWarnings("unchecked")
	private synchronized void restoreMenu() {
		try {
			log.trace("Trying to deserialize key list from preferences");
			byte[] data = preferences.getBytes(preferencesKey);
			if (data != null) {
				ObjectInputStream ins = new ObjectInputStream(
						new ByteArrayInputStream(data));

				// Generates unchecked warnings but we know
				ArrayList<StringMenuItem<T>> restoredKeys = (ArrayList<StringMenuItem<T>>) ins
						.readObject();

				for (final StringMenuItem<T> item : restoredKeys) {
					addItemInternal(item);
				}
			}
		} catch (InvalidDataTypeException e) {
			log.warn(e);
		} catch (BackendException e) {
			log.warn(e);
		} catch (IOException e) {
			log.warn(e);
		} catch (ClassNotFoundException e) {
			log.warn(e);
		}
	}

	@Override
	@SuppressWarnings("unchecked")
	public void clear() {
		ArrayList<StringMenuItem<T>> keyClone = (ArrayList<StringMenuItem<T>>) itemStrings
				.clone();
		for (StringMenuItem<T> item : keyClone) {
			removeItem(item);
		}
	}

	@Override
	public synchronized int getNumberOfItems() {
		int numKeys = itemStrings.size();
		log.trace("Getting the number of items on the menu: " + numKeys);
		return numKeys;
	}

	private void createJMenu(final String menuName, final Icon icon,
			final String clearName, final String clearTooltip) {
		log.trace("Creating new JMenu with name: " + menuName);
		this.menu = new JMenu(menuName);
		if (icon != null) {
			log.trace("Setting menu icon to " + icon);
			this.menu.setIcon(icon);
		}
		if (clearName != null) {
			this.menu.add(new ClearAction(clearName, clearTooltip));
			this.menu.add(new JSeparator(JSeparator.HORIZONTAL));
		}
	}

	private synchronized void addItemInternal(
			final StringMenuItem<T> stringMenuItem) {
		log.trace("Creating JMenu item");
		JMenuItem jMenuItem = createJMenuItem(stringMenuItem);

		log.trace("Adding cache entry for item: " + stringMenuItem);
		itemStringToJMenuItem.put(stringMenuItem, jMenuItem);

		log.trace("Adding key to key list");
		itemStrings.add(stringMenuItem);

		log.trace("Addin JMenuItem: " + jMenuItem + " to JMenu");
		this.menu.insert(jMenuItem, 2);
	}

	private JMenuItem createJMenuItem(final StringMenuItem<T> stringMenuItem) {
		JMenuItem jMenuItem = new JMenuItem(
				stringMenuItem.getPresentationName());
		jMenuItem.addActionListener(new ActionListener() {
			public void actionPerformed(java.awt.event.ActionEvent e) {
				fireMenuItemSelected(stringMenuItem);
			}
		});
		return jMenuItem;
	}

	@SuppressWarnings("serial")
	class ClearAction extends AbstractTypedAction {

		public ClearAction(final String name, final String tooltip) {
			super();
			setName(name);
			if (tooltip != null) {
				setToolTip(tooltip);
			}
		}

		@Override
		public void actionPerformed(ActionEvent e) {
			clear();
		}
	}
}
